package cn.qu.netty.boot;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@SuppressWarnings("unchecked")
public class ApplicationContext implements InitializingBean {
	
	final static Log log = Log.createLog();

	public final static int order = 0;

	private final static int CLASS = ".class".length();

	private final HashMap<String, Object> beanMap = new HashMap<>();

	private String[] scannerPackage;

	public ApplicationContext(String[] scannerPackage) {
		try {
			this.scannerPackage = scannerPackage;
			String scannerPackagestr = "";
			for (String string : scannerPackage) {
				scannerPackagestr += string + " ";
			}
			log.log("scanner packages [" + scannerPackagestr + "]");
			this.loadBean();
			log.log("load bean success....");
			this.injection();
			log.log("dependency injection bean success....");
			this.afterPropertiesSet();
			log.log("bean afterPropertiesSet success....");
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public <T> T getBean(String beanName) {
		return (T) beanMap.get(beanName);
	}

	public <T> List<T> getBean(Class<T> clazz) {
		return this.getBean(obj -> instanceOf(obj, clazz));
	}

	public <A extends Annotation, T> List<T> getBeanByAnnotation(Class<A> annotationClass) {
		return this.getBean(obj -> {
			A annotation = obj.getClass().getAnnotation(annotationClass);
			return annotation != null;
		});
	}

	private <T> List<T> getBean(Function<Object, Boolean> func) {
		List<T> list = new ArrayList<>();
		Iterator<Object> iterator = beanMap.values().iterator();
		while (iterator.hasNext()) {
			Object next = iterator.next();
			if (func.apply(next)) {
				list.add((T) next);
			}
		}
		return list;
	}

	public <T> boolean instanceOf(Object obj, Class<T> clazz) {
		Class<? extends Object> clazz2 = obj.getClass();
		Class<?>[] interfaces = clazz2.getInterfaces();
		for (Class<?> class1 : interfaces) {
			if (class1 == clazz) {
				return true;
			}
		}

		while (clazz2 != null) {
			if (clazz2 == clazz) {
				return true;
			}
			clazz2 = clazz2.getSuperclass();
		}
		return false;
	}

	public void loadBean() throws Exception {
		String resourcePath = this.getClass().getResource("/").getPath().substring(1);
		String[] values = this.scannerPackage;
		List<String> list = Arrays.stream(values).distinct().collect(Collectors.toList());
		values = list.toArray(new String[list.size()]);
		for (int i = 0; i < values.length; i++) {
			for (int j = 0; j < values.length; j++) {
				if (j == i) {
					continue;
				}
				if (values[i].contains(values[j])) {
					list.remove(values[i]);
				}
			}
		}

		for (String value : list) {
			String packagePath = value.replaceAll("\\.", "/");
			String loadPackagePath = resourcePath + packagePath;
			List<File> dirs = new ArrayList<>();
			File file = new File(loadPackagePath);
			dirs.add(file);
			this.getDir(file, dirs);
			for (File dir : dirs) {
				this.loadBean(dir);
			}
		}
	}

	public void injection() throws IllegalArgumentException, IllegalAccessException {
		Iterator<Object> iterator = beanMap.values().iterator();
		while (iterator.hasNext()) {
			Object object = iterator.next();
			Field[] fields = object.getClass().getDeclaredFields();
			for (Field field : fields) {
				field.setAccessible(true);
				Autowired autowired = field.getAnnotation(Autowired.class);
				if (autowired != null) {
					String beanName = this.getBeanName(field.getType());
					field.set(object, beanMap.get(beanName));
				}
			}
		}
	}

	private String getBeanName(Class<?> clazz) {
		return this.getBeanName(clazz.getSimpleName());
	}

	private String getBeanName(String className) {
		String beanfirstName = className.substring(0, 1);
		return beanfirstName.toLowerCase() + className.substring(1);
	}

	private void getDir(File dir, List<File> list) {
		if (dir == null || !dir.isDirectory()) {
			return;
		}
		File[] listFiles = dir.listFiles(f -> f.isDirectory());
		list.addAll(Arrays.stream(listFiles).collect(Collectors.toList()));
		for (File file : listFiles) {
			getDir(file, list);
		}
	}

	private void loadBean(File dir) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		String resourcePath = this.getClass().getResource("/").getPath().substring(1);
		File[] listFiles = dir.listFiles(f -> !f.isDirectory());

		for (File classFile : listFiles) {
			String classFileName = classFile.getPath().replace(File.separator, "/");
			String className = classFileName.replace(resourcePath, "").replace("/", ".");
			className = className.substring(0, className.length() - CLASS);
			String name = classFile.getName().substring(0, classFile.getName().length() - CLASS);
			String beanName = this.getBeanName(name);
			Class<?> clazz = Class.forName(className);
			if (clazz.isInterface() || clazz.isEnum() || clazz.isAnnotation()
					|| Modifier.isAbstract(clazz.getModifiers())
					|| clazz.isAnnotationPresent(BootstrapApplication.class)) {
				continue;
			}

			if (clazz.isAnnotationPresent(Bean.class) || clazz.isAnnotationPresent(Controller.class)) {
				Object newInstance = clazz.newInstance();
				beanMap.put(beanName, newInstance);
			}
		}

		beanMap.put("applicationContext", this);
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		beanMap.values().stream().filter(o -> o != this && InitializingBean.class.isAssignableFrom(o.getClass()))
				.map(o -> (InitializingBean) o).sorted(Comparator.comparing(o -> {
					Method method;
					try {
						method = o.getClass().getMethod("order");
						int order = (int) method.invoke(o);
						return order;
					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				})).forEach(o -> {
					try {
						o.afterPropertiesSet();
					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				});

	}

	@Override
	public int order() {
		return InitializingBeanConstant.ApplicationContext;
	}

}
